import socket
import pygame

#############################################################
#变量定义
NO_KEY = 0
UP = 1  #游戏方向键，1234代表上下左右，0为空格
DOWN = 2
LEFT = 3
RIGHT = 4
SPACE = 5

#全局变量
my_id = 0  #储存自己被服务器分配的编号

#规定屏幕尺寸
box_size = 20  #格子大小
max_x_box = 45  #格子数量，x轴
max_y_box = round(max_x_box * 9 / 16)  #格子数量，y轴

screen_width = box_size * max_x_box  #屏幕宽度
screen_height = box_size * max_y_box  #屏幕高度

x_limit = screen_width - box_size  #最大x坐标
y_limit = screen_height - box_size  #最大y坐标

#窗口初始化
pygame.init()
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("贪吃蛇在线")

#服务器参数
server_ip = "192.168.0.191"  #服务器ipv4地址
server_port = 5555  #服务器端口号

#创建客户端socket，客户端socket不需要bind，只需connect，
#操作系统会为此socket自动分配ip和空闲port
connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print("connection socket建立, 发送参与游戏请求中...")

#颜色
screen_color = (66, 18, 87)  #背景颜色
food_color = (219, 57, 57)  #食物颜色
my_color = (158, 252, 76)  #蛇的颜色
enemy_color = (171, 171, 171)

#############################################################
#定义函数


#将收到的蛇身列表信息由string转化为列表
def unpack_map_data(raw):

    if False == raw.startswith("snakes:"):
        print("数据错误")

    #把蛇和食物信息分开
    raw_snakes, raw_food = raw.split("food:")

    #把蛇的信息由string转为数字
    raw_snakes = raw_snakes[10:-3]      #移除前缀"snakes:[[("和后缀")]]"
    raw_snakes = raw_snakes.split(")],[(")

    snakes = []
    for i, snake in enumerate(raw_snakes):
        snake = snake.split("),(")
        single_snake = []
        for j, xy in enumerate(snake):
            xy = xy.split(",")
            x = int(xy[0])
            y = int(xy[1])
            xy = (x, y)
            single_snake.append(xy)
        snakes.append(single_snake)

    #把食物信息由string转为数字
    print("test", raw_food[2:-2])   
    raw_food = raw_food[2:-2]       #移除前缀："[(" 移除后缀：")]"
    raw_food = raw_food.split("),(")

    food = []
    for i, xy in enumerate(raw_food):
        xy = xy.split(",")
        x = int(xy[0])
        y = int(xy[1])
        xy = (x, y)
        food.append(xy)
    return snakes, food


#在xy坐标画出一格color颜色的格子
def draw_box(xy, color):
    pygame.draw.rect(screen, color, (xy[0], xy[1], box_size, box_size))


#画出所有蛇
def draw_snakes(snakes, my_id):
    snake_count = len(snakes)
    for i in range(snake_count):
        if my_id == i:
            color = my_color
        else:
            color = enemy_color
        for xy in snakes[i]:
            draw_box(xy, color)


#画出食物
def draw_food(food):
    for xy in food:
        draw_box(xy, food_color)


#判断是否鼠标关闭游戏窗口,避免死机
def game_closed_by_X():
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            return True


#############################################################
#定义主函数


def main():

    #连接到服务器,
    try:
        connection.connect((server_ip, server_port))
        print("连接成功")
    except:
        print("连接失败，退出游戏")
        connection.close()
        quit()

    #开始匹配阶段
    while True:
        data = connection.recv(256).decode()  #重复接收来自服务器的匹配信息
        #服务器拒绝服务，人数已满，退出程序
        if (data.startswith("msg(拒绝)")):
            print(data)
            connection.close()
            quit()

        #服务器同意加入游戏，全局变量my_id记录本客户端编号
        elif (data.startswith("msg(同意)")):
            print(data)
            global my_id
            my_id = int(data.split("为：")[1])
            print(f"my_id: {my_id}")

        #服务器匹配完成
        elif (data.startswith("msg(匹配完成)")):
            print(data)
            msg = (f"msg: 客户端 {my_id} 收到指令，准备就绪")  #回传准备就绪
            connection.sendall(str.encode(msg))
            break  #退出匹配阶段

    #游戏阶段---------------------------------------------------------------
    #检测按键并在合适的时候发送到服务器
    clock = pygame.time.Clock()  #用于循环体内clock.tick()
    #每秒60次循环，每次循环记为1帧，客户端每3帧发送一个信号，服务器接受后返回所有食物和蛇的信息
    #服务器根据收到的按键里有无加速键，每3帧或9帧更新一次客户端蛇的逻辑状态
    frame_of_9 = 1  #frame_of_9记录这一帧是每个9帧里的第几帧
    keys_buff = []  #按键缓存

    game_running = True
    while game_running:

        clock.tick(60)  #clock（n）使while循环每秒精确执行n次
        pygame.event.pump()  #pump()防止按键检测不能用。get_pressed()需要所在循环中有其他event对象的调用

        keys = pygame.key.get_pressed()  #得到的是一个字典，里面每个按键对应一个bool值，True即被按

        #收集控制蛇的按键信息，每帧检测一次，录入按键缓存
        key_list = []
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            key_list.append(LEFT)
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            key_list.append(RIGHT)
        if keys[pygame.K_UP] or keys[pygame.K_w]:
            key_list.append(UP)
        if keys[pygame.K_DOWN] or keys[pygame.K_s]:
            key_list.append(DOWN)
        if keys[pygame.K_SPACE]:
            key_list.append(SPACE)

        #若key_list不空，表明此轮循环有按键输入，更新缓存
        if key_list:
            keys_buff = key_list

        if frame_of_9 % 3 == 0:  #每3帧发送一次信息

            if not keys_buff:  #若无按键，添加无按键标识
                keys_buff.append(NO_KEY)
            if frame_of_9 % 9 == 0:  #若是第9帧发的信息，添加第9帧标识
                keys_buff.append(9)
            data = str(keys_buff) + "|"  #"|"分隔符
            data = data.replace(" ", "")  #去掉空格
            connection.sendall(str.encode(data))  #发送给服务器
            print(data)
            keys_buff.clear()  #清空按键缓存

            #接收来自服务器的信息，画出图形
            data = connection.recv(8192).decode()
            print("got msg\data: " + data)
            if data.startswith("msg(game over)"):  #如果收到附加信息，做处理
                game_running = False
                print(data)
            else:
                #画出图形
                snakes, food = unpack_map_data(data)  #提取蛇坐标string为list
                screen.fill(screen_color)  #画出背景
                draw_snakes(snakes, my_id)  #画出蛇，my_id用于让画出函数区分敌我颜色
                draw_food(food)
                pygame.display.update()

        if frame_of_9 == 9:  #第9帧结束，复原帧的标记
            frame_of_9 = 1
        else:  #普通帧结束，帧数自加1
            frame_of_9 += 1

        if game_closed_by_X():
            pygame.quit()
            break

    connection.close()
    print("connection socket关闭")


#############################################################
#主函数入口
if __name__ == "__main__":
    main()
